package jp.chiheisen.popuptail.tail;

import javax.swing.event.EventListenerList;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.nio.file.*;

import static java.nio.file.StandardWatchEventKinds.ENTRY_MODIFY;
import static java.nio.file.StandardWatchEventKinds.OVERFLOW;

/**
 * ファイルの末尾を監視するスレッドです。
 * 指定したファイルの末尾に追記が行われると、TailEvent を発行します。
 * TailEvent には、前回からの差分を含みます。
 *
 * ファイルが短くなったときは、イベントを発行しません。
 * ただし、短くなったあとに追記された場合、イベントを発行します。
 *
 * @author YujiYamamoto
 * @since 1.0
 */
public class Tail implements Runnable{

    /** イベントリスナー */
    private EventListenerList listenerList = new EventListenerList();
    /** 監視対象のファイル */
    private final File file;

    /** ファイルの末尾 */
    private long tailPos;

    public Tail(File file) {
        if(file == null){
            throw new NullPointerException("file");
        }
        assert file.exists();
        assert file.isFile();

        this.file = file;
    }

    public void run() {
        try(WatchService watcher = FileSystems.getDefault().newWatchService()){
            Path parentDir = file.toPath().getParent();
            parentDir.register(watcher, ENTRY_MODIFY);

            Path realtiveFile = parentDir.relativize(file.toPath());
            resetPos();

            while(true) {
                // wait for key to be signaled
                WatchKey key = watcher.take();

                for (WatchEvent<?> event : key.pollEvents()) {
                    WatchEvent.Kind<?> kind = event.kind();

                    if (kind == OVERFLOW) {
                        continue;
                    }

                    @SuppressWarnings({"unchecked"})
                    WatchEvent<Path> ev = (WatchEvent<Path>) event;
                    Path eventFile = ev.context();

                    if(file.toPath().endsWith(eventFile)){
                        byte[] tail = getTail();

                        if(tail.length != 0){
                            notifyTailEvent(new TailEvent(this, tail));
                        }
                    }
                }

                // Reset the key -- this step is critical to receive further watch events.
                boolean valid = key.reset();
                if (!valid) {
                    break;
                }
            }
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            // thread end.
        }
    }

    private void resetPos() throws IOException {
        try(RandomAccessFile random = new RandomAccessFile(file, "r")){
            this.tailPos = random.length();
        }
    }

    private byte[] getTail() throws IOException {
        byte[] tail = new byte[0];
        try(RandomAccessFile random = new RandomAccessFile(file, "r")){
            if(this.tailPos < random.length()){
                random.seek(tailPos);

                tail = readToEnd(random);
            }
            this.tailPos = random.length();
        }
        return tail;
    }

    private byte[] readToEnd(RandomAccessFile random) throws IOException {
        ByteArrayOutputStream out = new ByteArrayOutputStream();
        int buf;
        while((buf = random.read()) != -1){
            out.write(buf);
        }

        return out.toByteArray();
    }

    private void notifyTailEvent(TailEvent event) {
        for (TailListener listener : listenerList.getListeners(TailListener.class)) {
            listener.modify(event);
        }
    }

    public void addTailListener(TailListener listener) {
        this.listenerList.add(TailListener.class, listener);
    }

    public void removeTailListener(TailListener listener) {
        this.listenerList.remove(TailListener.class, listener);
    }
}